#!/usr/bin/env python
# utils/build-script - The ultimate tool for building Swift -*- python -*-
#
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See https://swift.org/LICENSE.txt for license information
# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors

from __future__ import print_function

import argparse
import os
import pipes
import platform
import sys
import time

from build_swift import defaults
from build_swift import driver_arguments
from build_swift import presets
from build_swift.migration import migrate_swift_sdks

from swift_build_support.swift_build_support import (
    arguments,
    debug,
    diagnostics,
    migration,
    products,
    shell,
    tar,
    targets,
    workspace
)
from swift_build_support.swift_build_support.SwiftBuildSupport import (
    HOME,
    SWIFT_BUILD_ROOT,
    SWIFT_REPO_NAME,
    SWIFT_SOURCE_ROOT,
)
from swift_build_support.swift_build_support.cmake import CMake
from swift_build_support.swift_build_support.targets import \
    StdlibDeploymentTarget
from swift_build_support.swift_build_support.toolchain import host_toolchain

build_script_impl = os.path.join(
    SWIFT_SOURCE_ROOT, SWIFT_REPO_NAME, "utils", "build-script-impl")


def exit_rejecting_arguments(message, parser=None):
    print(message, file=sys.stderr)
    if parser:
        parser.print_usage(sys.stderr)
    sys.exit(2)  # 2 is the same as `argparse` error exit code.


def call_without_sleeping(command, env=None, dry_run=False, echo=False):
    """
    Execute a command during which system sleep is disabled.

    By default, this ignores the state of the `shell.dry_run` flag.
    """

    # Disable system sleep, if possible.
    if platform.system() == 'Darwin':
        # Don't mutate the caller's copy of the arguments.
        command = ["caffeinate"] + list(command)

    shell.call(command, env=env, dry_run=dry_run, echo=echo)


class HostSpecificConfiguration(object):

    """Configuration information for an individual host."""

    def __init__(self, host_target, invocation):
        """Initialize for the given `host_target`."""

        # Compute the set of deployment targets to configure/build.
        args = invocation.args
        if host_target == args.host_target:
            # This host is the user's desired product, so honor the requested
            # set of targets to configure/build.
            stdlib_targets_to_configure = args.stdlib_deployment_targets
            if "all" in args.build_stdlib_deployment_targets:
                stdlib_targets_to_build = set(stdlib_targets_to_configure)
            else:
                stdlib_targets_to_build = set(
                    args.build_stdlib_deployment_targets).intersection(
                    set(args.stdlib_deployment_targets))
        else:
            # Otherwise, this is a host we are building as part of
            # cross-compiling, so we only need the target itself.
            stdlib_targets_to_configure = [host_target]
            stdlib_targets_to_build = set(stdlib_targets_to_configure)

        # Compute the lists of **CMake** targets for each use case (configure
        # vs. build vs. run) and the SDKs to configure with.
        self.sdks_to_configure = set()
        self.swift_stdlib_build_targets = []
        self.swift_test_run_targets = []
        self.swift_benchmark_build_targets = []
        self.swift_benchmark_run_targets = []
        for deployment_target_name in stdlib_targets_to_configure:
            # Get the target object.
            deployment_target = StdlibDeploymentTarget.get_target_for_name(
                deployment_target_name)
            if deployment_target is None:
                diagnostics.fatal("unknown target: %r" % (
                    deployment_target_name,))

            # Add the SDK to use.
            deployment_platform = deployment_target.platform
            self.sdks_to_configure.add(deployment_platform.sdk_name)

            # If we aren't actually building this target (only configuring
            # it), do nothing else.
            if deployment_target_name not in stdlib_targets_to_build:
                continue

            # Compute which actions are desired.
            build = (
                deployment_platform not in invocation.platforms_to_skip_build)
            test = (
                deployment_platform not in invocation.platforms_to_skip_test)
            test_host_only = None
            dt_supports_benchmark = deployment_target.supports_benchmark
            build_benchmarks = build and dt_supports_benchmark
            build_external_benchmarks = all([build, dt_supports_benchmark,
                                             args.build_external_benchmarks])

            # FIXME: Note, `build-script-impl` computed a property here
            # w.r.t. testing, but it was actually unused.

            # For platforms which normally require a connected device to
            # test, the default behavior is to run tests that only require
            # the host (i.e., they do not attempt to execute).
            if deployment_platform.is_darwin and \
               deployment_platform.is_embedded and \
               not deployment_platform.is_simulator:
                if deployment_platform not in \
                        invocation.platforms_to_skip_test_host:
                    test_host_only = True
                    test = True
                else:
                    test = False

            name = deployment_target.name

            for skip_test_arch in invocation.platforms_archs_to_skip_test:
                if deployment_target.name == skip_test_arch.name:
                    test = False

            if build:
                # Validation, long, and stress tests require building the full
                # standard library, whereas the other targets can build a
                # slightly smaller subset which is faster to build.
                if args.build_swift_stdlib_unittest_extra or \
                        args.validation_test or args.long_test or \
                        args.stress_test:
                    self.swift_stdlib_build_targets.append(
                        "swift-stdlib-" + name)
                else:
                    self.swift_stdlib_build_targets.append(
                        "swift-test-stdlib-" + name)
            if build_benchmarks:
                self.swift_benchmark_build_targets.append(
                    "swift-benchmark-" + name)
                # FIXME: This probably should respect `args.benchmark`, but
                # a typo in build-script-impl meant we always would do this.
                self.swift_benchmark_run_targets.append(
                    "check-swift-benchmark-" + name)

            if build_external_benchmarks:
                # Add support for the external benchmarks.
                self.swift_benchmark_build_targets.append(
                    "swift-benchmark-{}-external".format(name))
                self.swift_benchmark_run_targets.append(
                    "check-swift-benchmark-{}-external".format(name))
            if test:
                if test_host_only:
                    suffix = "-non-executable"
                else:
                    suffix = ""
                subset_suffix = ""
                if args.validation_test and args.long_test and \
                        args.stress_test:
                    subset_suffix = "-all"
                elif args.validation_test:
                    subset_suffix = "-validation"
                elif args.long_test:
                    subset_suffix = "-only_long"
                elif args.stress_test:
                    subset_suffix = "-only_stress"
                else:
                    subset_suffix = ""
                self.swift_test_run_targets.append("check-swift{}{}-{}".format(
                    subset_suffix, suffix, name))
                if args.test_optimized and not test_host_only:
                    self.swift_test_run_targets.append(
                        "check-swift{}-optimize-{}".format(
                            subset_suffix, name))
                if args.test_optimize_for_size and not test_host_only:
                    self.swift_test_run_targets.append(
                        "check-swift{}-optimize_size-{}".format(
                            subset_suffix, name))


class BuildScriptInvocation(object):

    """Represent a single build script invocation."""

    @staticmethod
    def validate_arguments(toolchain, args):
        if toolchain.cc is None or toolchain.cxx is None:
            diagnostics.fatal(
                "can't find clang (please install clang-3.5 or a "
                "later version)")

        if toolchain.cmake is None:
            diagnostics.fatal("can't find CMake (please install CMake)")

        if args.distcc:
            if toolchain.distcc is None:
                diagnostics.fatal(
                    "can't find distcc (please install distcc)")
            if toolchain.distcc_pump is None:
                diagnostics.fatal(
                    "can't find distcc-pump (please install distcc-pump)")

        if args.host_target is None or args.stdlib_deployment_targets is None:
            diagnostics.fatal("unknown operating system")

        if args.symbols_package:
            if not os.path.isabs(args.symbols_package):
                print(
                    '--symbols-package must be an absolute path '
                    '(was \'{}\')'.format(args.symbols_package))
                return 1
            if not args.install_symroot:
                diagnostics.fatal(
                    "--install-symroot is required when specifying "
                    "--symbols-package.")

        if args.android:
            if args.android_ndk is None or \
                    args.android_api_level is None or \
                    args.android_icu_uc is None or \
                    args.android_icu_uc_include is None or \
                    args.android_icu_i18n is None or \
                    args.android_icu_i18n_include is None:
                diagnostics.fatal(
                    "when building for Android, --android-ndk, "
                    "--android-ndk-version, --android-icu-uc, "
                    "--android-icu-uc-include, --android-icu-i18n, "
                    "and --android-icu-i18n-include must be specified")

    @staticmethod
    def apply_default_arguments(toolchain, args):
        # Infer if ninja is required
        ninja_required = (
            args.cmake_generator == 'Ninja' or args.build_foundation)
        if ninja_required and toolchain.ninja is None:
            args.build_ninja = True

        # Set the default stdlib-deployment-targets, if none were provided.
        if args.stdlib_deployment_targets is None:
            stdlib_targets = \
                StdlibDeploymentTarget.default_stdlib_deployment_targets()
            args.stdlib_deployment_targets = [
                target.name for target in stdlib_targets]

        # SwiftPM and XCTest have a dependency on Foundation.
        # On OS X, Foundation is built automatically using xcodebuild.
        # On Linux, we must ensure that it is built manually.
        if ((args.build_swiftpm or args.build_xctest) and
                platform.system() != "Darwin"):
            args.build_foundation = True

        # Foundation has a dependency on libdispatch.
        # On OS X, libdispatch is provided by the OS.
        # On Linux, we must ensure that it is built manually.
        if (args.build_foundation and
                platform.system() != "Darwin"):
            args.build_libdispatch = True

        if args.build_subdir is None:
            args.build_subdir = \
                workspace.compute_build_subdir(args)

        # --test-paths implies --test and/or --validation-test
        # depending on what directories/files have been specified.
        if args.test_paths:
            for path in args.test_paths:
                if path.startswith('test'):
                    args.test = True
                elif path.startswith('validation-test'):
                    args.test = True
                    args.validation_test = True

        # Add optional stdlib-deployment-targets
        if args.android:
            args.stdlib_deployment_targets.append(
                StdlibDeploymentTarget.Android.armv7.name)

        # Infer platform flags from manually-specified configure targets.
        # This doesn't apply to Darwin platforms, as they are
        # already configured. No building without the platform flag, though.

        android_tgts = [tgt for tgt in args.stdlib_deployment_targets
                        if StdlibDeploymentTarget.Android.contains(tgt)]
        if not args.android and len(android_tgts) > 0:
            args.android = True
            args.build_android = False

# ---

    def __init__(self, toolchain, args):
        self.toolchain = toolchain
        self.args = args

        self.workspace = workspace.Workspace(
            source_root=SWIFT_SOURCE_ROOT,
            build_root=os.path.join(SWIFT_BUILD_ROOT, args.build_subdir))

        # Compute derived information from the arguments.
        #
        # FIXME: We should move the platform-derived arguments to be entirely
        # data driven, so that we can eliminate this code duplication and just
        # iterate over all supported platforms.

        self.platforms_to_skip_build = set()
        if not args.build_linux:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.Linux)
        if not args.build_freebsd:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.FreeBSD)
        if not args.build_cygwin:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.Cygwin)
        if not args.build_osx:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.OSX)
        if not args.build_ios_device:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.iOS)
        if not args.build_ios_simulator:
            self.platforms_to_skip_build.add(
                StdlibDeploymentTarget.iOSSimulator)
        if not args.build_tvos_device:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.AppleTV)
        if not args.build_tvos_simulator:
            self.platforms_to_skip_build.add(
                StdlibDeploymentTarget.AppleTVSimulator)
        if not args.build_watchos_device:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.AppleWatch)
        if not args.build_watchos_simulator:
            self.platforms_to_skip_build.add(
                StdlibDeploymentTarget.AppleWatchSimulator)
        if not args.build_android:
            self.platforms_to_skip_build.add(StdlibDeploymentTarget.Android)

        self.platforms_to_skip_test = set()
        self.platforms_archs_to_skip_test = set()
        if not args.test_linux:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.Linux)
        if not args.test_freebsd:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.FreeBSD)
        if not args.test_cygwin:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.Cygwin)
        if not args.test_osx:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.OSX)
        if not args.test_ios_host:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.iOS)
        else:
            exit_rejecting_arguments("error: iOS device tests are not " +
                                     "supported in open-source Swift.")
        if not args.test_ios_simulator:
            self.platforms_to_skip_test.add(
                StdlibDeploymentTarget.iOSSimulator)
        if not args.test_ios_32bit_simulator:
            self.platforms_archs_to_skip_test.add(
                StdlibDeploymentTarget.iOSSimulator.i386)
        if not args.test_tvos_host:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.AppleTV)
        else:
            exit_rejecting_arguments("error: tvOS device tests are not " +
                                     "supported in open-source Swift.")
        if not args.test_tvos_simulator:
            self.platforms_to_skip_test.add(
                StdlibDeploymentTarget.AppleTVSimulator)
        if not args.test_watchos_host:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.AppleWatch)
        else:
            exit_rejecting_arguments("error: watchOS device tests are not " +
                                     "supported in open-source Swift.")
        if not args.test_watchos_simulator:
            self.platforms_to_skip_test.add(
                StdlibDeploymentTarget.AppleWatchSimulator)

        if not args.test_android_host:
            self.platforms_to_skip_test.add(StdlibDeploymentTarget.Android)

        self.platforms_to_skip_test_host = set()
        if not args.test_ios_host:
            self.platforms_to_skip_test_host.add(StdlibDeploymentTarget.iOS)
        if not args.test_tvos_host:
            self.platforms_to_skip_test_host.add(
                StdlibDeploymentTarget.AppleTV)
        if not args.test_watchos_host:
            self.platforms_to_skip_test_host.add(
                StdlibDeploymentTarget.AppleWatch)

    def initialize_runtime_environment(self):
        """Change the program environment for building."""

        # Set an appropriate default umask.
        os.umask(0o022)

        if 'SDKROOT' in os.environ:
            print('NOTE: Environment variable SDKROOT is set to "{}"'
                  .format(os.environ['SDKROOT']))

        # Unset environment variables that might affect how tools behave.
        for v in [
                'MAKEFLAGS',
                'MACOSX_DEPLOYMENT_TARGET',
                'IPHONEOS_DEPLOYMENT_TARGET',
                'TVOS_DEPLOYMENT_TARGET',
                'WATCHOS_DEPLOYMENT_TARGET']:
            os.environ.pop(v, None)

    def build_ninja(self):
        if not os.path.exists(self.workspace.source_dir("ninja")):
            diagnostics.fatal(
                "can't find source directory for ninja "
                "(tried %s)" % (self.workspace.source_dir("ninja")))

        ninja_build = products.Ninja(
            args=self.args,
            toolchain=self.toolchain,
            source_dir=self.workspace.source_dir("ninja"),
            build_dir=self.workspace.build_dir("build", "ninja"))
        ninja_build.do_build()
        self.toolchain.ninja = ninja_build.ninja_bin_path

    def convert_to_impl_arguments(self):
        """convert_to_impl_arguments() -> (env, args)

        Convert the invocation to an environment and list of arguments suitable
        for invoking `build-script-impl`.
        """

        # Create local shadows, for convenience.
        args = self.args
        toolchain = self.toolchain

        cmake = CMake(args=args,
                      toolchain=self.toolchain)

        impl_args = [
            "--workspace", self.workspace.source_root,
            "--build-dir", self.workspace.build_root,
            "--install-prefix", args.install_prefix,
            "--host-target", args.host_target,
            "--stdlib-deployment-targets",
            " ".join(args.stdlib_deployment_targets),
            "--host-cc", toolchain.cc,
            "--host-cxx", toolchain.cxx,
            "--darwin-xcrun-toolchain", args.darwin_xcrun_toolchain,
            "--darwin-deployment-version-osx=%s" % (
                args.darwin_deployment_version_osx),
            "--darwin-deployment-version-ios=%s" % (
                args.darwin_deployment_version_ios),
            "--darwin-deployment-version-tvos=%s" % (
                args.darwin_deployment_version_tvos),
            "--darwin-deployment-version-watchos=%s" % (
                args.darwin_deployment_version_watchos),
            "--cmake", toolchain.cmake,
            "--cmark-build-type", args.cmark_build_variant,
            "--llvm-build-type", args.llvm_build_variant,
            "--swift-build-type", args.swift_build_variant,
            "--swift-stdlib-build-type", args.swift_stdlib_build_variant,
            "--lldb-build-type", args.lldb_build_variant,
            "--lldb-build-with-xcode", args.lldb_build_with_xcode,
            "--foundation-build-type", args.foundation_build_variant,
            "--libdispatch-build-type", args.libdispatch_build_variant,
            "--libicu-build-type", args.libicu_build_variant,
            "--xctest-build-type", args.build_variant,
            "--swiftpm-build-type", args.build_variant,
            "--llbuild-build-type", args.build_variant,
            "--swift-enable-assertions", str(args.swift_assertions).lower(),
            "--swift-stdlib-enable-assertions", str(
                args.swift_stdlib_assertions).lower(),
            "--swift-analyze-code-coverage", str(
                args.swift_analyze_code_coverage).lower(),
            "--llbuild-enable-assertions", str(
                args.llbuild_assertions).lower(),
            "--lldb-assertions", str(
                args.lldb_assertions).lower(),
            "--cmake-generator", args.cmake_generator,
            "--build-jobs", str(args.build_jobs),
            "--common-cmake-options=%s" % ' '.join(
                pipes.quote(opt) for opt in cmake.common_options()),
            "--build-args=%s" % ' '.join(
                pipes.quote(arg) for arg in cmake.build_args()),
        ]

        # Compute any product specific cmake arguments.
        for product_class in self.compute_product_classes():
            product_name = product_class.product_name()
            product_source_name = product_class.product_source_name()
            source_dir = self.workspace.source_dir(product_source_name)

            if not os.path.exists(source_dir):
                diagnostics.fatal(
                    "can't find source directory for %s "
                    "(tried %s)" % (product_name, source_dir))

            product = product_class(
                args=args,
                toolchain=self.toolchain,
                source_dir=source_dir,
                # FIXME: This is incorrect since it always assumes the host
                # target I think?
                build_dir=self.workspace.build_dir(
                    args.host_target, product_name))
            cmake_opts = product.cmake_options

            # FIXME: We should be using pipes.quote here but we run into issues
            # with build-script-impl/cmake not being happy with all of the
            # extra "'" in the strings. To fix this easily, we really need to
            # just invoke cmake from build-script directly rather than futzing
            # with build-script-impl. This makes even more sense since there
            # really isn't a security issue here.
            impl_args += [
                "--%s-cmake-options=%s" % (product_name, ' '.join(cmake_opts))
            ]

        if args.build_stdlib_deployment_targets:
            impl_args += [
                "--build-stdlib-deployment-targets", " ".join(
                    args.build_stdlib_deployment_targets)]
        if args.cross_compile_hosts:
            impl_args += [
                "--cross-compile-hosts", " ".join(args.cross_compile_hosts)]

        if args.test_paths:
            impl_args += ["--test-paths", " ".join(args.test_paths)]

        if toolchain.ninja:
            impl_args += ["--ninja-bin=%s" % toolchain.ninja]
        if args.distcc:
            impl_args += [
                "--distcc",
                "--distcc-pump=%s" % toolchain.distcc_pump
            ]

        # *NOTE* We use normal cmake to pass through tsan/ubsan options. We do
        # NOT pass them to build-script-impl.
        if args.enable_asan:
            impl_args += ["--enable-asan"]
            # If we are on linux, disable leak detection when running ASAN. We
            # have a separate bot that checks for leaks.
            if platform.system() == 'Linux':
                os.environ['ASAN_OPTIONS'] = 'detect_leaks=0'
        if args.enable_ubsan:
            impl_args += ["--enable-ubsan"]

        # If we have lsan, we need to export our suppression list. The actual
        # passing in of the LSAN flag is done via the normal cmake method. We
        # do not pass the flag to build-script-impl.
        if args.enable_lsan:
            supp_file = os.path.join(SWIFT_SOURCE_ROOT, SWIFT_REPO_NAME,
                                     "utils",
                                     "lsan_leaks_suppression_list.txt")
            os.environ['LSAN_OPTIONS'] = 'suppressions={}'.format(supp_file)
        if args.verbose_build:
            impl_args += ["--verbose-build"]
        if args.install_symroot:
            impl_args += [
                "--install-symroot", os.path.abspath(args.install_symroot)
            ]

        if args.skip_build:
            impl_args += ["--skip-build-cmark",
                          "--skip-build-llvm",
                          "--skip-build-swift"]
        if not args.build_benchmarks:
            impl_args += ["--skip-build-benchmarks"]
        # Currently we do not build external benchmarks by default.
        if args.build_external_benchmarks:
            impl_args += ["--skip-build-external-benchmarks=0"]
        if not args.build_foundation:
            impl_args += ["--skip-build-foundation"]
        if not args.build_xctest:
            impl_args += ["--skip-build-xctest"]
        if not args.build_lldb:
            impl_args += ["--skip-build-lldb"]
        if not args.build_llbuild:
            impl_args += ["--skip-build-llbuild"]
        if not args.build_libdispatch:
            impl_args += ["--skip-build-libdispatch"]
        if not args.build_libicu:
            impl_args += ["--skip-build-libicu"]
        if not args.build_swiftpm:
            impl_args += ["--skip-build-swiftpm"]
        if not args.build_playgroundsupport:
            impl_args += ["--skip-build-playgroundsupport"]
        if args.build_swift_dynamic_stdlib:
            impl_args += ["--build-swift-dynamic-stdlib"]
        if args.build_swift_static_stdlib:
            impl_args += ["--build-swift-static-stdlib"]
        if args.build_swift_stdlib_unittest_extra:
            impl_args += ["--build-swift-stdlib-unittest-extra"]
        if args.build_swift_dynamic_sdk_overlay:
            impl_args += ["--build-swift-dynamic-sdk-overlay"]
        if args.build_swift_static_sdk_overlay:
            impl_args += ["--build-swift-static-sdk-overlay"]

        if not args.build_linux:
            impl_args += ["--skip-build-linux"]
        if not args.build_freebsd:
            impl_args += ["--skip-build-freebsd"]
        if not args.build_cygwin:
            impl_args += ["--skip-build-cygwin"]
        if not args.build_osx:
            impl_args += ["--skip-build-osx"]
        if not args.build_ios_device:
            impl_args += ["--skip-build-ios-device"]
        if not args.build_ios_simulator:
            impl_args += ["--skip-build-ios-simulator"]
        if not args.build_tvos_device:
            impl_args += ["--skip-build-tvos-device"]
        if not args.build_tvos_simulator:
            impl_args += ["--skip-build-tvos-simulator"]
        if not args.build_watchos_device:
            impl_args += ["--skip-build-watchos-device"]
        if not args.build_watchos_simulator:
            impl_args += ["--skip-build-watchos-simulator"]
        if not args.build_android:
            impl_args += ["--skip-build-android"]

        if not args.test and not args.long_test and not args.stress_test:
            impl_args += ["--skip-test-swift"]
        if not args.test:
            impl_args += ["--skip-test-cmark",
                          "--skip-test-lldb",
                          "--skip-test-llbuild",
                          "--skip-test-swiftpm",
                          "--skip-test-xctest",
                          "--skip-test-foundation",
                          "--skip-test-libdispatch",
                          "--skip-test-libicu",
                          "--skip-test-playgroundsupport"]
        if not args.test_linux:
            impl_args += ["--skip-test-linux"]
        if not args.test_freebsd:
            impl_args += ["--skip-test-freebsd"]
        if not args.test_cygwin:
            impl_args += ["--skip-test-cygwin"]
        if not args.test_osx:
            impl_args += ["--skip-test-osx"]
        if not args.test_ios_host:
            impl_args += ["--skip-test-ios-host"]
        if not args.test_ios_simulator:
            impl_args += ["--skip-test-ios-simulator"]
        if not args.test_ios_32bit_simulator:
            impl_args += ["--skip-test-ios-32bit-simulator"]
        if not args.test_tvos_host:
            impl_args += ["--skip-test-tvos-host"]
        if not args.test_tvos_simulator:
            impl_args += ["--skip-test-tvos-simulator"]
        if not args.test_watchos_host:
            impl_args += ["--skip-test-watchos-host"]
        if not args.test_watchos_simulator:
            impl_args += ["--skip-test-watchos-simulator"]
        if not args.test_android_host:
            impl_args += ["--skip-test-android-host"]
        if args.build_runtime_with_host_compiler:
            impl_args += ["--build-runtime-with-host-compiler"]
        if args.validation_test:
            impl_args += ["--validation-test"]
        if args.long_test:
            impl_args += ["--long-test"]
        if args.stress_test:
            impl_args += ["--stress-test"]
        if not args.benchmark:
            impl_args += ["--skip-test-benchmarks"]
        if not args.test_optimized:
            impl_args += ["--skip-test-optimized"]
        if not args.test_optimize_for_size:
            impl_args += ["--skip-test-optimize-for-size"]

        if args.android:
            impl_args += [
                "--android-ndk", args.android_ndk,
                "--android-api-level", args.android_api_level,
                "--android-ndk-gcc-version",
                args.android_ndk_gcc_version,
                "--android-icu-uc", args.android_icu_uc,
                "--android-icu-uc-include", args.android_icu_uc_include,
                "--android-icu-i18n", args.android_icu_i18n,
                "--android-icu-i18n-include", args.android_icu_i18n_include,
            ]
        if args.android_deploy_device_path:
            impl_args += [
                "--android-deploy-device-path",
                args.android_deploy_device_path,
            ]

        if platform.system() == 'Darwin':
            impl_args += [
                "--toolchain-prefix",
                targets.darwin_toolchain_prefix(
                    args.install_prefix),
                "--host-lipo", toolchain.lipo,
            ]

        if toolchain.libtool is not None:
            impl_args += [
                "--host-libtool", toolchain.libtool,
            ]

        # If we have extra_swift_args, combine all of them together and then
        # add them as one command.
        if args.extra_swift_args:
            impl_args += [
                "--extra-swift-args",
                ";".join(args.extra_swift_args)]

        # If we have extra_cmake_options, combine all of them together and then
        # add them as one command.
        if args.extra_cmake_options:
            impl_args += [
                "--extra-cmake-options=%s" % ' '.join(
                    pipes.quote(opt) for opt in args.extra_cmake_options)
            ]

        if args.lto_type is not None:
            impl_args += [
                "--llvm-enable-lto=%s" % args.lto_type,
                "--swift-tools-enable-lto=%s" % args.lto_type
            ]
            if args.llvm_max_parallel_lto_link_jobs is not None:
                impl_args += [
                    "--llvm-num-parallel-lto-link-jobs=%s" %
                    min(args.llvm_max_parallel_lto_link_jobs, args.build_jobs)
                ]
            if args.swift_tools_max_parallel_lto_link_jobs is not None:
                impl_args += [
                    "--swift-tools-num-parallel-lto-link-jobs=%s" %
                    min(args.swift_tools_max_parallel_lto_link_jobs,
                        args.build_jobs)
                ]

        impl_args += args.build_script_impl_args

        if args.dry_run:
            impl_args += ["--dry-run"]

        if args.clang_profile_instr_use:
            impl_args += [
                "--clang-profile-instr-use=%s" %
                os.path.abspath(args.clang_profile_instr_use)
            ]

        if args.lit_args:
            impl_args += ["--llvm-lit-args=%s" % args.lit_args]

        if args.coverage_db:
            impl_args += [
                "--coverage-db=%s" %
                os.path.abspath(args.coverage_db)
            ]

        # Compute the set of host-specific variables, which we pass through to
        # the build script via environment variables.
        host_specific_variables = self.compute_host_specific_variables()
        impl_env = {}
        for (host_target, options) in host_specific_variables.items():
            for (name, value) in options.items():
                # We mangle into an environment variable we can easily evaluate
                # from the `build-script-impl`.
                impl_env["HOST_VARIABLE_{}__{}".format(
                    host_target.replace("-", "_"), name)] = value

        return (impl_env, impl_args)

    def compute_host_specific_variables(self):
        """compute_host_specific_variables(args) -> dict

        Compute the host-specific options, organized as a dictionary keyed by
        host of options.
        """

        args = self.args

        options = {}
        for host_target in [args.host_target] + args.cross_compile_hosts:
            # Compute the host specific configuration.
            config = HostSpecificConfiguration(host_target, self)

            # Convert into `build-script-impl` style variables.
            options[host_target] = {
                "SWIFT_SDKS": " ".join(sorted(
                    config.sdks_to_configure)),
                "SWIFT_STDLIB_TARGETS": " ".join(
                    config.swift_stdlib_build_targets),
                "SWIFT_BENCHMARK_TARGETS": " ".join(
                    config.swift_benchmark_build_targets),
                "SWIFT_RUN_BENCHMARK_TARGETS": " ".join(
                    config.swift_benchmark_run_targets),
                "SWIFT_TEST_TARGETS": " ".join(
                    config.swift_test_run_targets),
            }

        return options

    def compute_product_classes(self):
        """compute_product_classes() -> list

        Compute the list of all Product classes used in this build. This list
        is constructed in dependency order.
        """

        # FIXME: This is a weird division (returning a list of class objects),
        # but it matches the existing structure of the `build-script-impl`.

        product_classes = []
        product_classes.append(products.CMark)
        product_classes.append(products.LLVM)
        if self.args.build_libicu:
            product_classes.append(products.LibICU)
        product_classes.append(products.Swift)
        if self.args.build_lldb:
            product_classes.append(products.LLDB)
        if self.args.build_llbuild:
            product_classes.append(products.LLBuild)
        if self.args.build_libdispatch:
            product_classes.append(products.LibDispatch)
        if self.args.build_foundation:
            product_classes.append(products.Foundation)
        if self.args.build_xctest:
            product_classes.append(products.XCTest)
        if self.args.build_swiftpm:
            product_classes.append(products.SwiftPM)
        return product_classes

    def execute(self):
        """Execute the invocation with the configured arguments."""

        # Convert to a build-script-impl invocation.
        (impl_env, impl_args) = self.convert_to_impl_arguments()

        # If using the legacy implementation, delegate all behavior to
        # `build-script-impl`.
        if self.args.legacy_impl:
            # Execute the underlying build script implementation.
            call_without_sleeping([build_script_impl] + impl_args,
                                  env=impl_env, echo=True)
            return

        # Otherwise, we compute and execute the individual actions ourselves.

        def execute_one_impl_action(host=None, product_class=None, name=None):
            if host is None:
                assert (product_class is None and
                        name == "merged-hosts-lipo"), "invalid action"
                action_name = name
            elif product_class is None:
                assert name == "package", "invalid action"
                action_name = "{}-{}".format(host.name, name)
            else:
                assert name is not None, "invalid action"
                action_name = "{}-{}-{}".format(
                    host.name, product_class.product_name(), name)
            call_without_sleeping(
                [build_script_impl] + impl_args + [
                    "--only-execute", action_name],
                env=impl_env, echo=self.args.verbose_build)

        # Compute the list of hosts to operate on.
        all_host_names = [
            self.args.host_target] + self.args.cross_compile_hosts
        all_hosts = [StdlibDeploymentTarget.get_target_for_name(name)
                     for name in all_host_names]

        # Compute the list of product classes to operate on.
        #
        # FIXME: This should really be per-host, but the current structure
        # matches that of `build-script-impl`.
        product_classes = self.compute_product_classes()

        # Execute each "pass".

        # Build...
        for host_target in all_hosts:
            # FIXME: We should only compute these once.
            config = HostSpecificConfiguration(host_target.name, self)
            print("Building the standard library for: {}".format(
                " ".join(config.swift_stdlib_build_targets)))
            if config.swift_test_run_targets and (
                    self.args.test or self.args.long_test):
                print("Running Swift tests for: {}".format(
                    " ".join(config.swift_test_run_targets)))
            if config.swift_benchmark_run_targets and self.args.benchmark:
                print("Running Swift benchmarks for: {}".format(
                    " ".join(config.swift_benchmark_run_targets)))

            for product_class in product_classes:
                execute_one_impl_action(host_target, product_class, "build")

        # Test...
        for host_target in all_hosts:
            for product_class in product_classes:
                execute_one_impl_action(host_target, product_class, "test")

        # Install...
        for host_target in all_hosts:
            for product_class in product_classes:
                execute_one_impl_action(host_target, product_class, "install")

        # Package...
        for host_target in all_hosts:
            execute_one_impl_action(host_target, name="package")

        # Lipo...
        execute_one_impl_action(name="merged-hosts-lipo")


# Provide a short delay so accidentally invoked clean builds can be canceled.
def clean_delay():
    sys.stdout.write('Starting clean build in  ')
    for i in range(3, 0, -1):
        sys.stdout.write('\b%d' % i)
        sys.stdout.flush()
        time.sleep(1)
    print('\b\b\b\bnow.')


# Main entry point for the preset mode.
def main_preset():
    parser = argparse.ArgumentParser(
        formatter_class=argparse.RawDescriptionHelpFormatter,
        description="""Builds Swift using a preset.""")
    parser.add_argument(
        "-n", "--dry-run",
        help="print the commands that would be executed, but do not execute "
             "them",
        action="store_true",
        default=False)
    parser.add_argument(
        "--preset-file",
        help="load presets from the specified file",
        metavar="PATH",
        action="append",
        dest="preset_file_names",
        default=[])
    parser.add_argument(
        "--preset",
        help="use the specified option preset",
        metavar="NAME")
    parser.add_argument(
        "--show-presets",
        help="list all presets and exit",
        action=arguments.action.optional_bool)
    parser.add_argument(
        "--distcc",
        help="use distcc",
        action=arguments.action.optional_bool)
    parser.add_argument(
        "-j", "--jobs",
        help="the number of parallel build jobs to use",
        type=int,
        dest="build_jobs")
    parser.add_argument(
        "preset_substitutions_raw",
        help="'name=value' pairs that are substituted in the preset",
        nargs="*",
        metavar="SUBSTITUTION")
    parser.add_argument(
        "--expand-build-script-invocation",
        help="Print the expanded build-script invocation generated "
             "by the preset, but do not run the preset",
        action=arguments.action.optional_bool)
    args = parser.parse_args()

    if len(args.preset_file_names) == 0:
        args.preset_file_names = [
            os.path.join(
                SWIFT_SOURCE_ROOT, SWIFT_REPO_NAME, "utils",
                "build-presets.ini")
        ]

        user_presets_file = os.path.join(HOME, '.swift-build-presets')
        if os.path.isfile(user_presets_file):
            args.preset_file_names.append(user_presets_file)

    preset_parser = presets.PresetParser()

    try:
        preset_parser.read(args.preset_file_names)
    except presets.Error as e:
        diagnostics.fatal(e.message)

    if args.show_presets:
        for name in sorted(preset_parser.preset_names(),
                           key=lambda name: name.lower()):
            print(name)
        return 0

    if not args.preset:
        diagnostics.fatal("missing --preset option")

    args.preset_substitutions = {}
    for arg in args.preset_substitutions_raw:
        name, value = arg.split("=", 1)
        args.preset_substitutions[name] = value

    try:
        preset = preset_parser.get_preset(args.preset,
                                          vars=args.preset_substitutions)
    except presets.Error as e:
        diagnostics.fatal(e.message)

    preset_args = migrate_swift_sdks(preset.format_args())

    build_script_args = [sys.argv[0]]
    if args.dry_run:
        build_script_args += ["--dry-run"]

    build_script_args += preset_args
    if args.distcc:
        build_script_args += ["--distcc"]
    if args.build_jobs:
        build_script_args += ["--jobs", str(args.build_jobs)]

    diagnostics.note('using preset "{}", which expands to \n\n{}\n'.format(
        args.preset, shell.quote_command(build_script_args)))

    if args.expand_build_script_invocation:
        return 0

    call_without_sleeping(build_script_args)
    return 0


# Main entry point for the normal mode.
def main_normal():
    parser = driver_arguments.create_argument_parser()

    args = migration.parse_args(parser, sys.argv[1:])

    if args.build_script_impl_args:
        # If we received any impl args, check if `build-script-impl` would
        # accept them or not before any further processing.
        try:
            migration.check_impl_args(build_script_impl,
                                      args.build_script_impl_args)
        except ValueError as e:
            exit_rejecting_arguments(e, parser)

        if '--check-args-only' in args.build_script_impl_args:
            return 0

    shell.dry_run = args.dry_run

    # Prepare and validate toolchain
    if args.darwin_xcrun_toolchain is None:
        xcrun_toolchain = os.environ.get('TOOLCHAINS',
                                         defaults.DARWIN_XCRUN_TOOLCHAIN)

        diagnostics.note('Using toolchain {}'.format(xcrun_toolchain))
        args.darwin_xcrun_toolchain = xcrun_toolchain

    toolchain = host_toolchain(xcrun_toolchain=args.darwin_xcrun_toolchain)
    os.environ['TOOLCHAINS'] = args.darwin_xcrun_toolchain

    if args.host_cc is not None:
        toolchain.cc = args.host_cc
    if args.host_cxx is not None:
        toolchain.cxx = args.host_cxx
    if args.host_lipo is not None:
        toolchain.lipo = args.host_lipo
    if args.host_libtool is not None:
        toolchain.libtool = args.host_libtool
    if args.cmake is not None:
        toolchain.cmake = args.cmake

    # Preprocess the arguments to apply defaults.
    BuildScriptInvocation.apply_default_arguments(toolchain, args)

    # Validate the arguments.
    BuildScriptInvocation.validate_arguments(toolchain, args)

    # Create the build script invocation.
    invocation = BuildScriptInvocation(toolchain, args)

    # Sanitize the runtime environment.
    invocation.initialize_runtime_environment()

    # Show SDKs, if requested.
    if args.show_sdks:
        debug.print_xcodebuild_versions()

    # Clean build directory if requested.
    if args.clean:
        clean_delay()
        shell.rmtree(invocation.workspace.build_root)

    # Create build directory.
    shell.makedirs(invocation.workspace.build_root)

    # Build ninja if required, which will update the toolchain.
    if args.build_ninja:
        invocation.build_ninja()

    # Execute the underlying build script implementation.
    invocation.execute()

    if args.symbols_package:
        print('--- Creating symbols package ---')
        print('-- Package file: {} --'.format(args.symbols_package))

        if platform.system() == 'Darwin':
            prefix = targets.darwin_toolchain_prefix(args.install_prefix)
        else:
            prefix = args.install_prefix

        # As a security measure, `tar` normally strips leading '/' from paths
        # it is archiving. To stay safe, we change working directories, then
        # run `tar` without the leading '/' (we remove it ourselves to keep
        # `tar` from emitting a warning).
        with shell.pushd(args.install_symroot):
            tar.tar(source=prefix.lstrip('/'),
                    destination=args.symbols_package)

    return 0


def main():
    if not SWIFT_SOURCE_ROOT:
        diagnostics.fatal(
            "could not infer source root directory " +
            "(forgot to set $SWIFT_SOURCE_ROOT environment variable?)")

    if not os.path.isdir(SWIFT_SOURCE_ROOT):
        diagnostics.fatal(
            "source root directory \'" + SWIFT_SOURCE_ROOT +
            "\' does not exist " +
            "(forgot to set $SWIFT_SOURCE_ROOT environment variable?)")

    # Determine if we are invoked in the preset mode and dispatch accordingly.
    if any([(opt.startswith("--preset") or opt == "--show-presets")
            for opt in sys.argv[1:]]):
        return main_preset()
    else:
        return main_normal()


if __name__ == "__main__":
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(1)
